/**
 * 20220112 created by jordia
 * 20220607：add camera getter
 */
import {EventDispatcher, Matrix4, Raycaster, Vector2, Vector3} from "three";

const EVENT_SOURCE_TYPE={
    crossEvent:'crossEvent',
    xrEvent:'xrEvent',
};
const TARGET_RAY_MODE={
    trackedPointer:'tracked-pointer',
    gaze:'gaze',
    screen:'screen',
};

/**
 * event callback
 */
class EventCallback {
    constructor(eventSource,eventSourceType,eventCenter,raycaster,eventType){
        this.eventSource=eventSource;
        this.type=eventType;
        this.raycaster=raycaster;
        this.eventCenter=eventCenter;
        this.eventType=eventSourceType;
        this._init();
    }

    dispose(){
        this.eventSource.removeEventListener(this.type,this._updateRay);
    }

    _init(){
        this._initVariable();
        this._initEventBind();
    }
    _initVariable(){
        this.camera=null;
        this.rect=null;
        this.updateCall=null;
        this._tV0=new Vector2();
        this._tV1=new Vector3();
        this._tM0=new Matrix4();
        this._eventObject={};
    }
    _initEventBind(){
        this._updateRay=(e)=> {
            if (typeof this.updateCall === 'function') {
                this.updateCall(this.raycaster, e);
            } else {
                switch (this.eventType) {
                    case EVENT_SOURCE_TYPE.crossEvent:
                        if (!this.camera) {
                            console.warn('[camera] needs to be assigned first!');
                            return;
                        }
                        if (!this.rect) {
                            console.warn('[rect] needs to be set first! ');
                            return;
                        }
                        EventCallback.setCrossEventRaycast(this.raycaster, e, this._tV0, this._tV1, this.rect, this.camera);
                        break;
                    case EVENT_SOURCE_TYPE.xrEvent:
                        let tar = e.object;
                        if (tar.userData.inputSource && tar.userData.inputSource['targetRayMode']) {
                            switch (tar.userData.inputSource['targetRayMode']) {
                                case TARGET_RAY_MODE.trackedPointer:
                                    EventCallback.setXREventTrackedPointerRaycast(this.raycaster, e, this._tM0);
                                    break;
                                case TARGET_RAY_MODE.gaze:
                                    EventCallback.setXREventGazeRaycast(this.raycaster, e);
                                    break;
                                case TARGET_RAY_MODE.screen:
                                    EventCallback.setXREventScreenRaycast(this.raycaster, e);
                                    break;
                            }
                        } else {
                            console.warn('failed to set raycaster,unsupported event!');
                            return;
                        }
                        break;
                    default:
                        console.warn('unknown event-source-type!');
                }
            }
            this._eventObject.type = this.type;
            this._eventObject.raycaster = this.raycaster;
            this._eventObject.data = e;
            this.eventCenter.dispatchEvent(this._eventObject);
        };
        this.eventSource.addEventListener(this.type,this._updateRay);
    }

    /**
     * none-xr raycaster update method
     * @param raycaster
     * @param event
     * @param pointer
     * @param pointerVec
     * @param rect
     * @param camera
     */
    static setCrossEventRaycast(raycaster,event,pointer,pointerVec,rect,camera){
        if(!event)return;
        if(event.isMouse){
            pointer.set(event.clientX||0, event.clientY||0);
        }else{
            if(!event.touches)return;
            if(!event.touches[0])return;
            let tch=event.touches[0];
            pointer.set(tch.clientX||0, tch.clientY||0);
        }
        let rc = rect;
        let x = (pointer.x - rc.left) / rc.width;
        let y = (pointer.y - rc.top) / rc.height;
        pointerVec.set((x * 2) - 1, 1 - (y * 2));
        raycaster.setFromCamera(pointerVec, camera);
    }

    /**
     * tracked-pointer raycaster update method
     * @param raycaster
     * @param event
     * @param rot
     */
    static setXREventTrackedPointerRaycast(raycaster,event,rot){
        let ctr=event.object;
        rot.identity();
        let trs=ctr.matrixWorld;
        rot.extractRotation(trs);
        raycaster.ray.origin.setFromMatrixPosition(trs);
        raycaster.ray.direction.set(0,0,-1).applyMatrix4(rot);
    }

    /**
     * gaze raycaster update method
     * @param raycaster
     * @param event
     */
    static setXREventGazeRaycast(raycaster,event){
        console.warn('[setXREventGazeRaycast] needs to be implemented!',raycaster,event);
    }

    /**
     * screen raycaster update method
     * @param raycaster
     * @param event
     */
    static setXREventScreenRaycast(raycaster,event){
        console.warn('[setXREventScreenRaycast] needs to be implemented!',raycaster,event);
    }
}

/**
 * raycaster updater
 */
export default class Raycast{
    constructor(eventSource,eventSourceType=Raycast.eventSourceTypes.crossEvent){
        this._event=eventSource;
        this._eventType=eventSourceType;
        this._init();
    }

    //...............................................public...............................................

    /**
     * event center
     * @returns {EventDispatcher}
     */
    get eventCenter(){
        return this._eventCenter;
    }

    /**
     * raycaster
     * @returns {Raycaster}
     */
    get raycaster(){
        return this._raycaster;
    }

    /**
     * set the event types listen to
     * @param eventTypes
     */
    set events(eventTypes){
        if(eventTypes){
            this.events=null;
            this._eventTypes=eventTypes;
            for(let i=0;i<this._eventTypes.length;i++){
                let typ=this._eventTypes[i];
                let callback=new EventCallback(this._event,this._eventType,this._eventCenter,this._raycaster,typ);
                callback.camera=this._camera;
                callback.rect=this._rect;
                callback.updateCall=this._updateCall;
                this._callbacks.push(callback);
            }
        }else{
            for(let i=0;i<this._eventTypes;i++){
                this._callbacks[i].dispose();
            }
            this._eventTypes.length=0;
            this._callbacks.length=0;
        }
    }

    /**
     * get listened event types
     * @returns {*|[]}
     */
    get events(){
        return this._eventTypes;
    }

    /**
     * customize raycaster update method(ignore the default method)
     * @param func_raycaster_eventData
     */
    set setRaycaster(func_raycaster_eventData){
        this._updateCall=func_raycaster_eventData;
        for(let i=0;i<this._callbacks.length;i++){
            this._callbacks[i].updateCall=func_raycaster_eventData;
        }
    }

    /**
     * camera(none-xr needed)
     * @param camera
     */
    set camera(camera){
        this._camera=camera;
        for(let i=0;i<this._callbacks.length;i++){
            this._callbacks[i].camera=camera;
        }
    }
    get camera(){
        return this._camera;
    }

    /**
     * rect range(none-xr needed)
     * @param rect
     */
    set rect(rect){
        this._rect=rect;
        for(let i=0;i<this._callbacks.length;i++){
            this._callbacks[i].rect=rect;
        }
    }

    /**
     * all possible type of event source
     * @returns {{xrEvent: string, crossEvent: string}}
     */
    static get eventSourceTypes(){
        return EVENT_SOURCE_TYPE;
    }

    //................................................private.............................................

    _init(){
        this._initVariable();
    }
    _initVariable(){
        this._eventCenter=new EventDispatcher();
        this._raycaster=new Raycaster();
        this._eventTypes=[];
        this._updateCall=null;
        this._camera=null;
        this._rect=null;
        this._tV0=new Vector2();
        this._tV1=new Vector3();
        this._tM0=new Matrix4();
        this._callbacks=[];
    }
}